package ke24.ga.extension;

import java.util.*;

import org.jgap.*;
import org.jgap.impl.*;

import com.softhinker.ke24.unit2.MainFace.DataPanelCreator;
import com.softhinker.ke24.unit2.data.DataPanel;
import com.softhinker.ke24.unit2.settings.Settings;
import com.softhinker.ke24.unit2.solution.BetterSolutionListener;
import com.softhinker.ke24.unit2.solution.IterationListener;

import ke24.ga.LoadDistributionFitnessFunction;
import ke24.ga.Package;
import ke24.ga.extension.adaptation.feedback.DeterministicLoadDistributionSwappingMutationOperator;
import com.softhinker.ke24.unit2.solution.Evolver;

/**
 * Given 64 packages with different weights. Try to make 16 group with each
 * groups having (nearly) the same weight as the other groups.
 * <p>
 * 
 * @author tan
 */
public class VariousSizeLoadDistribution implements Evolver{

    /**
     * Holds the available packages, each with a specific weight
     */
    protected Package[] pkgs;

    //private int m_numEvolutions;
    protected Parameters params = new Parameters();
    protected Configuration conf;
    private LoadDistributionSwappingMutationOperator mutationOperator; 
    private DataPanel listener;
    private double previousFitnessValue = Double.MAX_VALUE;
    protected Genotype genotype;
    


    private int iteration = 0;

    private String mutationRateStrategy;

    private int iterationCount;

    private HashMap<Integer, Double> weightMap;
    /**
     * Constructor.
     * 
     * @throws Exception
     * 
     * @author tan
     */
    public VariousSizeLoadDistribution(String mutationOption) {        
        init(params, mutationOption);
        
    }
    
    public VariousSizeLoadDistribution(String mutationOption, Parameters params) {        
        this.params = params;        
        init(params, mutationOption);                
    }
    
    public  VariousSizeLoadDistribution(DataPanel listener, 
            int populationSize,
            int mutationRate,
            String mutationRateStrategy,
            int interationCount,
            Package[] packages) {
            if (interationCount > 0)
                this.params = new Parameters(interationCount,populationSize,mutationRate,1);
            else this.params = new Parameters(0,populationSize,mutationRate,1);
            this.listener = listener;
            this.mutationRateStrategy = mutationRateStrategy;
            this.iterationCount = interationCount;
            //fix mutation rate
            this.weightMap = createPackageMap(packages);
            init(params,mutationRateStrategy, packages);                
    }
    
    public void reset(Settings settings) {
        setParameters(new Parameters(1,settings.getPopulationSize(),settings.getMutationRate(),1));
        this.iteration = 0;
        this.previousFitnessValue = Double.MAX_VALUE;
        init(params,this.mutationRateStrategy, pkgs);      
        
    }
    

    protected void init(Parameters params2, String string, Package[] packages) {
        this.pkgs = packages;
        init(params2,string);        
    }

    protected void init(Parameters params, String mutationOption)  {
        try {
            makePackages();       
            this.conf = configureJGAP();          
            MutationOperatorFactory factory = new MutationOperatorFactory( conf,params);
            setMutationOperator(factory.getMutationOperatorFactory(mutationOption));
            this.conf.addGeneticOperator(getMutationOperator());
            
            this.genotype =  configureGenotype(this.conf);      
            getMutationOperator().reset(params.getMutationRate());   
            /*
            int numExec = params.getNumExecution();
            IChromosome[] result = new IChromosome[numExec];
           
            for (int i = 0; i < numExec; i++) {
                //Configuration.reset();
               //reset mutation rate for adaptation
      
                
                getMutationOperator().reset(params.getMutationRate());      
                genotype =  configureGenotype(this.conf);            
                result[i] = doEvolution();            
            }
            displayFinalResult(result);
            */
        }
        catch (InvalidConfigurationException e) {
            e.printStackTrace();
        }
        
    }
    


    private void displayFinalResult(IChromosome[] result) {
        if (result == null || result.length == 0) {
            System.out.println("No execution is run");
            return;
        }
        System.out.println("Summary after " + result.length + " runs: ");
        double best = result[0].getFitnessValue();
        double avg = 0;
        for (int i = 0; i < result.length; i++) {
            System.out.println("Fitness value at run + " + (i+1) + ":" + result[i].getFitnessValue());
            if (best > result[i].getFitnessValue()) best = result[i].getFitnessValue();
            avg += result[i].getFitnessValue();
        }
        avg /= result.length;
        
        System.out.println();
        System.out.println("Best fitness value: " + best);
        System.out.println("Average fitness value: " + avg);
    }

    /**
     * Sets up the configuration for the problem.
     * 
     * @return configured genotype
     * 
     * @throws Exception
     * 
     * @author tan
     */
    protected Configuration configureJGAP()  {
        Configuration.reset();
        Configuration gaConf = new DefaultConfiguration();
        try {
            
            gaConf.resetProperty(Configuration.PROPERTY_FITEVAL_INST);
            gaConf.setFitnessEvaluator(new DeltaFitnessEvaluator());
            // Just use a swapping operator instead of mutation and others.
            // ------------------------------------------------------------
            gaConf.getGeneticOperators().clear();
            //SwappingMutationOperator swapper = new LoadDistributionSwappingMutationOperator(gaConf, params.getMutationRate());
            //SwappingMutationOperator swapper = getMutationOperator();
            //gaConf.addGeneticOperator(swapper);
            // Setup some other parameters.
            // ----------------------------
            gaConf.setPreservFittestIndividual(true);
            gaConf.setKeepPopulationSizeConstant(true);
            // Set number of individuals (=tries) per generation.
            // --------------------------------------------------
            gaConf.setPopulationSize(params.getPopulationSize());
            
            int chromeSize = pkgs.length;
            IChromosome sampleChromosome = new Chromosome(gaConf,
                    new IntegerGene(gaConf), chromeSize);
            gaConf.setSampleChromosome(sampleChromosome);
            // Setup the important fitness function!
            // -------------------------------------
            gaConf
                    .setFitnessFunction(new LoadDistributionFitnessFunction(
                            pkgs));
            //
            }
        catch(InvalidConfigurationException e) {
            System.out.println("Invalid Configuration Exception");
            e.printStackTrace();
        }
            return gaConf;
    }
    
    
    
    protected Genotype configureGenotype(Configuration gaConf) {
        Genotype genotype = null;
        
        try {
            // Setup the structure with which to evolve the
            // solution of the problem.
            // --------------------------------------------
            
            genotype = Genotype.randomInitialGenotype(gaConf);
            // Now ensure that each number from 1..64 (representing the
            // indices of the packages) is represented by exactly one gene.
            // --> Suboptimal here, as randomized initialization becomes
            // obsolete (other solution would be more complicated).
            // ---------------------------------------------------------
            List chromosomes = genotype.getPopulation().getChromosomes();
            for (int i = 0; i < chromosomes.size(); i++) {
                IChromosome chrom = (IChromosome) chromosomes.get(i);
                for (int j = 0; j < chrom.size(); j++) {
                    Gene gene = (Gene) chrom.getGene(j);
                    gene.setAllele(new Integer(pkgs[j].getId()));
                }
            }
        } catch (InvalidConfigurationException e) {
            e.printStackTrace();
            System.exit(-2);
        }
        return genotype;
    }

    /**
     * Does the evolution until finished.
     * 
     * @param a_genotype
     *            the genotype to evolve
     * 
     * @author tan
     */
    public IChromosome doEvolution() {
        int m_numEvolutions = params.getNumEvolution();
        int progress = 0;
        int percentEvolution = m_numEvolutions / 100;
        for (int i = 0; i < m_numEvolutions; i++) {
            
            //a_genotype.evolve();
            evolve();
            
            // Print progress.
            // ---------------
            /*
            if (percentEvolution > 0 && i % percentEvolution == 0) {
                progress++;
                IChromosome fittest = a_genotype.getFittestChromosome();
                double fitness = fittest.getFitnessValue();
                
                System.out.println("Currently best solution has fitness "
                        + fitness);
                        
                printSolution(fittest);
                
            }
            */
        }
        // Print summary.
        // --------------
        IChromosome fittest = genotype.getFittestChromosome();
        /*System.out.println("Best solution has fitness "
                + fittest.getFitnessValue());*/
        
        Gene[] fittestGene = fittest.getGenes();
        
        //printSolution(fittest);
        
        //rearrange fittest to comply with the constraints lighter packages lay on top of heavier packages
        for (int i = 0; i < 8; i++) {
            rearrangePackages(i, fittestGene);
        }        
       
        //printSolution(fittest);
        return fittest;
    }

    
    
    public void evolve() {
        this.iteration++;
        genotype.evolve();
        IChromosome fittest = genotype.getFittestChromosome();
        double currentFitnessValue = fittest.getFitnessValue();
        Gene[] fittestGene = fittest.getGenes();
        //System.out.println("check gene validity = " + integrityCheck(fittestGene));
        //System.out.println("Prev = " + previousFitnessValue + " current = " + currentFitnessValue + " mutation rate = " + this.mutationOperator.getMutationRate());      
        this.listener.onNewIteration(this.iteration);
        this.listener.onNewMutationRate(this.mutationOperator.getMutationRate());
        
       //displayAverage(fittestGene);
        
        if(currentFitnessValue < previousFitnessValue){
            for (int i = 0; i < 8; i++) {
                rearrangePackages(i, fittestGene);
            } 
            this.previousFitnessValue = currentFitnessValue;
            this.listener.onBetterSolutionFound(new SolutionDataRetriever(pkgs,fittestGene, this.weightMap),currentFitnessValue);
        }
    }

    private void displayAverage(Gene[] fittestGene) {
        double avg = 0.0;
        for (Gene g : fittestGene) {
            Integer i = (Integer)(g.getAllele());
            avg += this.weightMap.get(i);            
        }
        System.out.println("----------------------Average weight = " + avg/fittestGene.length + "--------------------");
        
    }

    private void rearrangePackages(int group, Gene[] fittestGene) {
        //get the total weight for each level
        double[] weightArr = new double[4];        
        int base = group * 8;
        
        //load the package IDs
        int[] pkgIDs = new int[8];      
        ArrayList<Integer> bigPkgIDs = new ArrayList<Integer>();
        ArrayList<Integer> smallPkgIDs1 = new ArrayList<Integer>();
        ArrayList<Integer> smallPkgIDs2 = new ArrayList<Integer>();
        for (int i = 0; i < 8; i++) {
            pkgIDs[i] = ((Integer)fittestGene[base + i].getAllele()).intValue();           
            if (VariousSizePackage.isBig(pkgIDs[i])) {
                if (i < 4) bigPkgIDs.add(new Integer(pkgIDs[i]));
            }else {
                if (i < 4) smallPkgIDs1.add(new Integer(pkgIDs[i]));
                else smallPkgIDs2.add(new Integer(pkgIDs[i]));
            }
        }
        
        
        
        Integer[] bigPIDs = bigPkgIDs.toArray(new Integer[bigPkgIDs.size()]);
        Integer[] smallPIDs1 = smallPkgIDs1.toArray(new Integer[smallPkgIDs1.size()]);
        Integer[] smallPIDs2 = smallPkgIDs2.toArray(new Integer[smallPkgIDs2.size()]);
        
        sortDescending(bigPIDs);
        sortDescending(smallPIDs1);
        sortDescending(smallPIDs2);
        
        //System.out.println("rearrange for group " + group);
        //merge big and small
        int[] result = merge(bigPIDs,smallPIDs1,smallPIDs2);
        
        for (int i = 0; i < 8; i++) {
            fittestGene[base + i].setAllele(new Integer(result[i]));      
        }
    }
    
    public boolean integrityCheck(Gene[] genes) {
        int[] count = new int[48];
        for (int i = 0; i < genes.length; i++) {
            Integer pID = (Integer)genes[i].getAllele();
            count[pID.intValue()-1] ++;
        }
        for (int i = 0; i < count.length; i++) {
            if (i < 16) {
                if (count[i] != 2) return false;
            }else {
                if (count[i] != 1) return false;
            }
        }
        return true;                
    }

    private int[] merge(Integer[] bigPIDs, Integer[] smallPIDs1, Integer[] smallPIDs2) {
        int[] result = new int[8];
        int bigIndex = 0;
        int smallIndex = 0;
        
        //System.out.println("Big package ID:");
        //displayArray(bigPIDs);
       // System.out.println("Small package ID 1:");
        //displayArray(smallPIDs1);
        //System.out.println("Small package ID 2:");
        //displayArray(smallPIDs2);
        
        for (int i = 0; i < 4; i++) {
            //if the big packages is now empty or the weight is not as big as the sum of the two small package, add small packages first
            if (bigPIDs.length <= bigIndex || (smallPIDs1.length > smallIndex && pkgs[bigPIDs[bigIndex]].getWeight() < pkgs[smallPIDs1[smallIndex]].getWeight() + pkgs[smallPIDs2[smallIndex]].getWeight())) {
                result[i] = smallPIDs1[smallIndex];
                result[i+4] = smallPIDs2[smallIndex];
                smallIndex ++;
            }else {
                result[i] = result[i + 4] = bigPIDs[bigIndex];
                bigIndex++;
            }
        }
        
        //System.out.println("Sort result:");
        //displayArray(result);
        return result;
    }

    private void sortDescending(Integer[] arr) {
        if (arr.length < 2) return;
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = arr.length - 2; j >= i; j--) {
                //if (pkgs[arr[j].intValue()].getWeight() < pkgs[arr[j+1].intValue()].getWeight()) {
                if (this.weightMap.get(arr[j]) < this.weightMap.get(arr[j+1])) {
                    //swap
                    Integer tmp = new Integer(arr[j].intValue());
                    arr[j] = new Integer(arr[j + 1].intValue());
                    arr[j + 1] = tmp;
                }
            }
        }
        
    }

    /**
     * @param a_solution
     *            a solution to print to the console
     * 
     * @author tan
     */
    public void printSolution(IChromosome a_solution) {
        double groupWeights = 0.0d;
        for (int i = 0; i < 16; i++) {
            System.out.println("\nGroup " + i);
            System.out.println("-------");
            double groupWeight = 0.0d;
            for (int j = 0; j < 4; j++) {
                IntegerGene pkgIndex = (IntegerGene) a_solution
                        .getGene((i * 4 + j));
                double weight = this.weightMap.get(((Integer)pkgIndex.getAllele()));
                groupWeight += weight;
                System.out.println("  Package at index " + pkgIndex.intValue()
                        + " with weight " + weight);
            }
            groupWeights += groupWeight;
            System.out.println("  --> Group weight: " + groupWeight);
        }
        System.out.println("\n Average group weight: " + groupWeights / 16);
    }
    /**
     * Create vents with different weights.
     * 
     * @author tan
     */
    public void makePackages() {
        if (this.pkgs != null) return;
        /*
         * double[] weightArr = { 27,57,33,55, 27,57,33,55, 43,40,27,62,
         * 43,40,27,62, 46,31,58,59, 46,31,58,59, 38,65,51,30, 38,65,51,30,
         * 66,63,53,37, 53,67,32,44, 53,47,39,38, 46,67,36,30, 37,43,68,50,
         * 64,32,32,59, 24,67,54,23, 49,60,65,35};
         */      
        /*
        int[] packageIds = {1,2,3,4,1,2,3,4,5,6,7,8,5,6,7,8,
                                        9,10,11,12,9,10,11,12,13,14,15,16,13,14,15,16,
                                        17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,
                                        33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48};
    
        //System.out.println("valid = " + isValidArrangment(packageIds));        
        
        double[] weightArr = {27, 57, 33, 55, 43, 40, 27, 62,
                                            46, 31, 58, 59, 38, 65, 51, 30,
                                            66, 63, 53, 37, 53, 67, 32, 44, 
                                            53, 47, 39, 38, 46, 67, 36, 30, 
                                            37, 43, 68, 50, 64, 32, 32, 59, 
                                            24, 67, 54, 23, 49, 60, 65, 35};
        // displayArray(weightArr);
         */
        // to rearrange to satisfy the conditions that lighter packages lay on
        // top of heavier packages
        //it's not needed anymore
        //weightArr = arrangeInitialPackages(weightArr);

        // displayArray(weightArr);
        Package[] big = PackageGenerator.generatePackage(1, 16, 100, 400);
        Package[] small = PackageGenerator.generatePackage(17, 48, 50, 200);
        
        //generate package array of 64 places that hold 48 pakcages
        pkgs = new Package[64];
         
        for (int i = 0; i < pkgs.length; i++) {
            // Set a weight between 290 and 310
            // double weight = 290 + Math.random() * 20;            
            if (i < 32) {                
                int id = (i / 8) * 4 + i % 4;
                
                
                pkgs[i] = new Package(big[id].getId(),big[id].getWeight()/2);
            }else {
                pkgs[i] = new Package(small[i - 32].getId(),small[i - 32].getWeight());
            }
            
        }
        this.weightMap = createPackageMap(pkgs);
        //displayArray(pkgs);
        //System.exit(0);
    }
    
    private HashMap<Integer, Double> createPackageMap(Package[] packages) {
        HashMap<Integer,Double> map = new HashMap<Integer, Double>();
        for (Package p : packages) {
            map.put(new Integer(p.getId()), new Double(p.getWeight()));
        }
        return map;
    }

    private void displayArray(double[] weightArr) {
        System.out
                .println("====================Start Display Array========================");
        int itemPerLine = 4;
        for (int i = 0; i < weightArr.length; i++) {
            if (i % itemPerLine == 0 && i != 0)
                System.out.println();
            System.out.print(weightArr[i] + "\t");
        }
        System.out.println();
        System.out
                .println("====================End Display Array========================");
    }
    
    private void displayArray(int[] arr) {
        System.out
                .println("====================Start Display Array========================");
        int itemPerLine = 4;
        for (int i = 0; i < arr.length; i++) {
            if (i % itemPerLine == 0 && i != 0)
                System.out.println();
            System.out.print(arr[i] + "\t");
        }
        System.out.println();
        System.out
                .println("====================End Display Array========================");
    }

    private void displayArray(Integer[] arr) {
        System.out
                .println("====================Start Display Array========================");
        int itemPerLine = 4;
        for (int i = 0; i < arr.length; i++) {
            if (i % itemPerLine == 0 && i != 0)
                System.out.println();
            System.out.print(arr[i].intValue() + "\t");
        }
        System.out.println();
        System.out
                .println("====================End Display Array========================");
    }
    
    private void displayArray(Package[] arr) {
        System.out
                .println("====================Start Display Array========================");
        int itemPerLine = 4;
        for (int i = 0; i < arr.length; i++) {
            if (i % itemPerLine == 0 && i != 0)
                System.out.println();
            System.out.print(arr[i].getId() + "-" + arr[i].getWeight() + "\t");
        }
        System.out.println();
        System.out
                .println("====================End Display Array========================");
    }
    
    
    public static int getOtherHalfPosition(int pos) {
        int rem = pos % 8;
        if (rem < 4) return pos  + 4;
        else return pos - 4;        
    }
    
    /**
     * check if the arrangment is valid
     * @param packageIds the package ID position
     * return if the arrangement is valid
     */
    public static boolean isValidArrangment(int[] pIds) {        
        if (pIds == null || pIds.length != 64) return false;
        for (int i = 0; i < pIds.length; i++) {
            if (pIds[i] <= 16) {
                int rem = i % 8;
                if (rem < 4 && pIds[i] != pIds[i+4]) return false;
                if (rem >= 4 && pIds[i] != pIds[i-4]) return false;
            }
        }
        return true;
    }
    
    /**
     * Start the example
     * 
     * @param args
     *            ignored
     * 
     * @author tan
     */
    public static void main(String[] args) {
        try {
            //new VariousSizeLoadDistribution();
            new VariousSizeLoadDistribution("deterministic", new Parameters(100,256,100,1));
        } catch (Throwable t) {
            t.printStackTrace();
            System.exit(1);
        }
    }

    public void setParameters(Parameters params) {
        this.params = params;
    }

    public Parameters getParameters() {
        return params;
    }

    public void setMutationOperator(LoadDistributionSwappingMutationOperator mutationOperator) {
        this.mutationOperator = mutationOperator;
    }

    public LoadDistributionSwappingMutationOperator getMutationOperator() {
        return mutationOperator;
    }
    
    
}
